﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Caching;
using System.Web;
using System.Threading;

namespace GoodStuff.Web
{
    /// <summary>
    /// Threadsafe generic HttpCache wrapper. Simplified HttpRunTime cache access and supports easy caching patterns.
    /// </summary>
    public abstract class Cached
    {
        private static object __cacheLock = new object();
        private static Dictionary<string, ReaderWriterLockSlim> __registeredKeys = new Dictionary<string, ReaderWriterLockSlim>();
        private static Dictionary<string, CacheGroup> __registeredGroups = new Dictionary<string, CacheGroup>();

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TData"></typeparam>
        /// <param name="cacheKey"></param>
        /// <param name="groupName"></param>
        /// <param name="slidingExpiration"></param>
        /// <param name="populateMethod"></param>
        /// <returns></returns>
        public static TData Get<TData>(string cacheKey, string groupName, TimeSpan slidingExpiration, Func<TData> populateMethod)
        {
            return Get(cacheKey, groupName, null, System.Web.Caching.Cache.NoAbsoluteExpiration, slidingExpiration, CacheItemPriority.Normal, populateMethod);
        }

        public static TData Get<TData>(string cacheKey, string groupName, DateTime absoluteExpiration, Func<TData> populateMethod)
        {
            return Get(cacheKey, groupName, null, absoluteExpiration, System.Web.Caching.Cache.NoSlidingExpiration, CacheItemPriority.Normal, populateMethod);
        }

        public static TData Get<TData, TVaryBy>(string cacheKey, TVaryBy varyBy, string groupName, TimeSpan slidingExpiration, Func<TData> populateMethod) where TVaryBy : struct
        {
            return Get(cacheKey, varyBy, groupName, null, System.Web.Caching.Cache.NoAbsoluteExpiration, slidingExpiration, CacheItemPriority.Normal, populateMethod);
        }

        public static TData Get<TData, TVaryBy>(string cacheKey, TVaryBy varyBy, string groupName, DateTime absoluteExpiration, Func<TData> populateMethod) where TVaryBy : struct
        {
            return Get(cacheKey, varyBy, groupName, null, absoluteExpiration, System.Web.Caching.Cache.NoSlidingExpiration, CacheItemPriority.Normal, populateMethod);
        }

        public static TData Get<TData, TVaryBy>(string cacheKey, TVaryBy varyBy, string groupName, CacheDependency dependency, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, Func<TData> populateMethod) where TVaryBy : struct
        {
            return Get(cacheKey + varyBy.ToString(), groupName, null, absoluteExpiration, slidingExpiration, CacheItemPriority.Normal, populateMethod);
        }

        /// <summary>
        /// Retrieve an object from the runtime cache. The populate method will fill the cache if is now yet created or expired.
        /// </summary>
        /// <typeparam name="TData"></typeparam>
        /// <param name="cacheKey"></param>
        /// <param name="slidingExpiration"></param>
        /// <param name="populateMethod"></param>
        /// <param name="absoluteExpiration"></param>
        /// <param name="groupName">The name of the cache group</param>
        /// <param name="dependency"></param>
        /// <param name="priority"></param>
        /// <returns></returns>
        public static TData Get<TData>(string cacheKey, string groupName, CacheDependency dependency, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, Func<TData> populateMethod) 
        {
            // read unlocked
            CacheItem<TData> item = HttpRuntime.Cache.Get(cacheKey) as CacheItem<TData>;
            if (item == null)
            {
                bool needsToLoad = false;
                ReaderWriterLockSlim perCacheKeyLock = null;

                lock (__cacheLock)
                {
                    //are we already reading this key?
                    //get the lock handle.
                    if (__registeredKeys.ContainsKey(cacheKey) == false)
                    {
                        perCacheKeyLock = new ReaderWriterLockSlim();
                        __registeredKeys.Add(cacheKey, perCacheKeyLock);
                        System.Diagnostics.Trace.WriteLine("Create new lock " + cacheKey);
                    }
                    else
                    {
                        System.Diagnostics.Trace.WriteLine("Use existing lock " + cacheKey);
                        perCacheKeyLock = __registeredKeys[cacheKey];
                    }
                }

                if (perCacheKeyLock.WaitingWriteCount > 0)
                {
                    System.Diagnostics.Trace.WriteLine("CallLock was held for " + cacheKey);
                }
                perCacheKeyLock.EnterWriteLock();
                try
                {
                    lock (__cacheLock)
                    {
                        item = HttpRuntime.Cache.Get(cacheKey) as CacheItem<TData>;
                        if (item == null)
                        {
                            needsToLoad = true;
                        }
                        else
                        {
                            System.Diagnostics.Trace.WriteLine("post-lock cache hit for " + cacheKey);
                        }
                    }

                    //lock is now released to allow concurrency.

                    if (needsToLoad)
                    {
                        TData value = populateMethod.Invoke();
                        item = new CacheItem<TData>() { Item = value };

                        //lock again to write data.
                        lock (__cacheLock)
                        {
                            if (__registeredGroups.ContainsKey(groupName))
                            {
                                __registeredGroups[groupName].SubKeys.Add(cacheKey);
                            }
                            else
                            {
                                __registeredGroups.Add(groupName, new CacheGroup() { SubKeys = new List<string>() { cacheKey } });
                            }

                            HttpRuntime.Cache.Add(cacheKey, item, dependency, absoluteExpiration, slidingExpiration, priority, null);
                            System.Diagnostics.Trace.WriteLine("Cache updated for " + cacheKey);
                        }
                    }
                }
                finally
                {
                    perCacheKeyLock.ExitWriteLock();
                }
            }
            else
            {
                System.Diagnostics.Trace.WriteLine("unlocked cache hit for " + cacheKey);
            }

            return item.Item;
        }

        /// <summary>
        /// Remove a key from the cache. 
        /// </summary>
        /// <param name="cacheKey"></param>
        public static void Remove(string cacheKey)
        {
            lock (__cacheLock)
            {
                __registeredKeys.Remove(cacheKey);

                HttpRuntime.Cache.Remove(cacheKey);
            }
        }

        /// <summary>
        /// Clear all cacheitems that were added by this cache.
        /// </summary>
        public static void RemoveAll()
        {
            lock (__cacheLock)
            {
                //Cannot do ForEach, since we are modifying the collection
                while (__registeredKeys.Count > 0)
                {
                    Remove(__registeredKeys.Keys.First());
                }
            }
        }

        /// <summary>
        /// Removes an entire group from the cache.
        /// </summary>
        /// <param name="groupName"></param>
        public static void RemoveGroup(string groupName)
        {
            lock (__cacheLock)
            {
                if (__registeredGroups.ContainsKey(groupName))
                {
                    CacheGroup group = __registeredGroups[groupName];
                    foreach (string subkey in group.SubKeys)
                    {
                        Remove(subkey);
                    }
                    __registeredGroups.Remove(groupName);
                }

            }
        }

        /// <summary>
        /// Helper class to support cache groups
        /// </summary>
        private struct CacheGroup
        {
            /// <summary>
            /// A list of all the keys that were registered with this group.
            /// </summary>
            public List<string> SubKeys;
        }

        private class CacheItem<T>
        {
            public T Item { get; set; }
        }
    }
}
